💡 AI 인사이트

🤖 AI가 여기에 결과를 출력합니다...

댓글 커뮤니티

쿠팡이벤트

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

검색

    로딩 중이에요... 🐣

    [코담] 웹개발·실전 프로젝트·AI까지, 파이썬·장고의 모든것을 담아낸 강의와 개발 노트

    04 요약정리 | ✅ 저자: 이유정(박사)

    쇼핑몰 크롤링을 위한 정보 제공 및 디스코드 챗봇 구현 코드


    🧭 프로젝트 흐름 요약

    이 프로젝트는 무신사(Musinsa) 쇼핑몰 데이터를 크롤링하고, 디스코드 챗봇과 연동하여 사용자 요청에 따라 실시간으로 상품 정보를 제공합니다.

    📌 전체 동작 흐름

    1. 사용자가 디스코드 채팅에서 신발추천 같은 명령어를 입력

    2. bot.py가 메시지를 인식하고 scraper.py를 호출하여 Musinsa 상품 데이터를 수집

    3. 크롤링된 데이터를 기반으로 추천 리스트, 최저가 상품, 상세 정보 등 분석

    4. 결과를 디스코드 Embed 메시지로 포맷팅하여 사용자에게 응답

    ✅ 디스코드 서버에서만 동작하며, 웹사이트 챗봇처럼 iframe 삽입은 불가능합니다.


    📦 프로젝트 구조 개요

    • .env: Discord 토큰 환경변수 저장 (예: DISCORD_TOKEN=your_token_here)

    • scraper.py: Musinsa 상품 정보를 가져오는 API 크롤러 클래스 정의

    • bot.py: 디스코드 챗봇 실행 코드 (아래 전체 코드 참고)


    ✅ scraper.py

    import requests
    
    from typing import List, Dict
    
    class MusinsaAPI():
        def __init__(self, keyword:str = "할인", page: int = 1, size: int = 60):
            # API 엔드포인트
            self.url = "https://api.musinsa.com/api2/dp/v1/plp/goods"
            # 요청 파라미터 설정
            self.params ={
                "gf": "A",
                "keyword": keyword,
                "sortCode": "POPULAR",
                "page": page,
                "size": size,
                "caller": "SEARCH",
            }
    
            self.headers = {
                "User-Agent":(
                    "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
                    "AppleWebKit/537.36 (KHTML, like Gecko)"
                    "Chrome/138.0.0.0 Safari/537.36"
            ),
            "Accept":"application/json, text/plain, */*",
            }
    
        # [{"keyword": keyword, "page": page}] # Get요청 수행
        def fetch(self) -> List[Dict]:  
            response = requests.get(self.url, params=self.params, headers=self.headers)
            response.raise_for_status()
            data = response.json()
    
      
            # 딕셔너리에서 안전하게 값을 꺼내오는 값을 조회하고 조건 검사하는 구문이다.
            goods_list = data.get("data", {}).get("list", [])
            if not goods_list:
                return []
    
      
    
            result: List[Dict] = []
    
            for g in goods_list:
    
                result.append({
                    "goodsNo": g.get("goodsNo"),
                    "name": g.get("goodsName", ""),
                    "brand": g.get("brandName", ""),
                    "normalPrice": f"{g.get("normalPrice", 0)}원",
                    "saleprice": f"{g.get("price", 0)}원",
                    "linkUrl": "https://api.musinsa.com/" + g.get("goodsLinkUrl", ""),
                    "imageUrl": g.get("imageUrl", ""),
                })  
    
            return result  
    
    # # 실행 및 출력
    api = MusinsaAPI()
    items = api.fetch()
    
    
    for item in items:
        name = item.get("goodsName")
        brand = item.get("brandName")
        gender = item.get("displayGenderText", "")
        price = item.get("price")
        normal_price = item.get("normalPrice")
        discount = item.get("saleRate", 0)
        review_count = item.get("reviewCount", 0)
        review_score = item.get("reviewScore", 0)
        link = item.get("linkUrl")
        image = item.get("imageUrl")
    
        # 라벨 예: ["타임세일"]
        labels = [label.get("title") for label in item.get("imageLabelList", [])]
    
        print(f"🎁 {name} ({brand}) - {gender}")
        print(f"💰 {normal_price}원 → {price}원 ({discount}% 할인)")
        print(f"⭐ 리뷰: {review_score}/100 ({review_count}개)")
        print(f"🏷️ 라벨: {', '.join(labels) if labels else '없음'}")
        print(f"🔗 링크: {link}")
        print(f"🖼️ 이미지: {image}")
        print("-" * 60)
    

    bot.py 전체 코드

    from dotenv import load_dotenv
    import os
    import re
    import discord
    from scraper import MusinsaAPI
    
    # ─── 1. 환경 변수 로딩 ─────────────────────────────
    load_dotenv()
    
    # ─── 2. 디스코드 봇 클라이언트 설정 ───────────────
    intents = discord.Intents.default()
    intents.message_content = True
    client = discord.Client(intents=intents)
    
    # ─── 3. Embed 메시지 생성 함수 ───────────────────
    def build_message(item):
        embed = discord.Embed(type="rich", title=item.get("name", "No name"), url=item.get("url", ""))
        embed.set_thumbnail(url=item.get("image", ""))
        embed.description = item.get("brand", "No brand")
        embed.add_field(name="정가", value=item.get("originalPrice", "-"), inline=True)
        embed.add_field(name="할인가", value=item.get("salePrice", "-"), inline=True)
        return embed
    
    # ─── 4. 채널별 추천 캐시 ──────────────────────────
    last_recommendations: dict[int, dict] = {}
    
    # ─── 5. 봇 실행 준비 이벤트 ───────────────────────
    @client.event
    async def on_ready():
        print("▶ Running file:", __file__)
        print(f"Logged in as {client.user}")
    
    # ─── 6. 메시지 처리 이벤트 ───────────────────────
    @client.event
    async def on_message(message):
        if message.author == client.user:
            return
    
        content = message.content.strip()
        content_lower = content.lower()
        channel_id = message.channel.id
    
        print(f"[디버그] 메시지 수신: {content!r}")
    
        # ─── 신발 추천 ─────────────────────
        if ("신발추천" in content_lower or "신발 추천" in content_lower or content_lower == "신발"):
            items = MusinsaAPI(keyword="신발", size=5).fetch() or []
            last_recommendations[channel_id] = {
                "keyword": "신발",
                "page": 1,
                "items": items
            }
            reply = "\n".join(f"{i+1}번. {item.get('name', '이름없음')}{item.get('salePrice', '-')}" for i, item in enumerate(items))
            await message.channel.send(f"👟 **신발 추천 TOP5**\n{reply}")
            return
    
        # ─── 반팔 추천 ─────────────────────
        if ("반팔추천" in content_lower or "반팔 추천" in content_lower or "티셔츠추천" in content_lower or "티셔츠 추천" in content_lower):
            items = MusinsaAPI(keyword="반팔", size=5).fetch() or []
            last_recommendations[channel_id] = {
                "keyword": "반팔",
                "page": 1,
                "items": items
            }
            reply = "\n".join(f"{i+1}번. {item.get('name', '이름없음')}{item.get('salePrice', '-')}" for i, item in enumerate(items))
            await message.channel.send(f"👕 **반팔 추천 TOP5**\n{reply}")
            return
    
        # ─── 상세보기 ─────────────────────
        m = re.match(r"(\d+)번 상품 상세", content_lower)
        if m and channel_id in last_recommendations:
            idx = int(m.group(1)) - 1
            items = last_recommendations[channel_id].get("items") or []
            if 0 <= idx < len(items):
                item = items[idx]
                await message.channel.send(embed=build_message(item))
            else:
                await message.channel.send("❌ 해당 상품 번호를 찾을 수 없습니다.")
            return
    
        # ─── 찜하기 ─────────────────────
        m = re.match(r"(\d+)번 상품 찜", content_lower)
        if m and channel_id in last_recommendations:
            await message.channel.send("💖 찜했어요! (가상 기능입니다)")
            return
    
        # ─── 가장 저렴한 상품 ─────────────────────
        if "가장 저렴" in content_lower and channel_id in last_recommendations:
            items = last_recommendations[channel_id].get("items") or []
            try:
                cheapest = min(items, key=lambda x: int(x.get("salePrice", "0").replace("원", "").replace(",", "")))
                await message.channel.send(f"💸 가장 저렴한 상품:\n{cheapest.get('name', '이름없음')}{cheapest.get('salePrice', '-')}")
            except:
                await message.channel.send("❌ 가격 정보를 비교할 수 없습니다.")
            return
    
        # ─── 다음 페이지 ─────────────────────
        if "다음 페이지" in content_lower and channel_id in last_recommendations:
            cache = last_recommendations[channel_id]
            new_page = cache["page"] + 1
            items = MusinsaAPI(keyword=cache["keyword"], page=new_page, size=5).fetch() or []
            last_recommendations[channel_id] = {
                "keyword": cache["keyword"],
                "page": new_page,
                "items": items
            }
            reply = "\n".join(f"{i+1}번. {item.get('name', '이름없음')}{item.get('salePrice', '-')}" for i, item in enumerate(items))
            await message.channel.send(f"📄 **{cache['keyword']} 추천 페이지 {new_page}**\n{reply}")
            return
    
        # ─── 기타 반응 ─────────────────────
        if content_lower in ("안녕하세요", "안녕", "안녕하십니까"):
            await message.channel.send(f"{message.author.display_name}님, 안녕하세요! 😊")
        if content_lower.startswith("$hello"):
            await message.channel.send("Hello!")
        if content_lower.startswith("$hi") or content_lower == "hi":
            await message.channel.send(f"hi, {message.author.display_name}! 🙂")
        if content_lower.startswith("!타임세일"):
            embeds = [build_message(i) for i in MusinsaAPI().fetch()[:10]]
            await message.channel.send(embeds=embeds)
    
    # ─── 7. 메인 진입점 ───────────────────────────────
    if __name__ == "__main__":
        token = os.getenv("DISCORD_TOKEN")
        if not token:
            raise RuntimeError("❌ DISCORD_TOKEN 환경변수가 설정되지 않았습니다.")
        client.run(token)
    

    🧪 디스코드 봇 사용 안내

    봇 실행 후 콘솔에 다음과 같이 출력되면 정상 작동입니다:

    ▶ Running file: D:/경로/bot.py
    Logged in as 봇이름#태그
    

    1️⃣ 봇 초대 링크

    아래 링크로 자신의 디스코드 서버에 봇을 초대해야 합니다:

    👉 https://discord.com/oauth2/authorize?client_id=1390211542007677089&permissions=2&scope=bot

    2️⃣ 디스코드 채팅에서 명령어 입력 예시

    입력 내용 동작 설명
    신발추천 신발 관련 TOP5 상품 추천 목록 전송
    2번 상품 상세 2번째 상품의 상세 Embed 메시지 전송
    3번 상품 찜 "찜했어요" 메시지 전송 (가상 기능)
    가장 저렴 추천 목록 중 최저가 상품 출력
    다음 페이지 다음 페이지 상품 리스트 출력
    안녕하세요, $hello, hi 인사 메시지 응답

    ⚠️ 주의사항

    • .env 파일에 DISCORD_TOKEN 설정이 필수입니다.

    • scraper.py에서 크롤링 로직이 정확해야 정보가 제대로 나타납니다.

    • 출력 값이 None이라면 크롤링 대상 사이트의 구조 변경을 의심해보세요.

    필요시 scraper.py 예제 코드도 제공해드릴 수 있습니다.

    TOP
    preload preload